Documentation
inbox/Authentication Quick Start.md
Authentication Quick Start
1-Line Integration ✨
Adding authentication to any Acsis component is now a single line of code!
Step 1: Add Authentication (1 line!)
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddAcsisDbContext<YourDb>("your-schema");
// 🔐 ADD AUTHENTICATION
builder.AddAcsisAuthentication();
var app = builder.Build();
// Authentication/authorization middleware is auto-included in MapAcsisEndpoints!
app.MapAcsisEndpoints(YourApi.MapYourEndpoints);
app.Run();
Step 2: Protect Endpoints (1 line!)
public static class YourApi
{
public static void MapYourEndpoints(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/items")
.WithTags("Items")
.RequireAuthorization(); // 🔐 Entire group protected!
group.MapGet("/", GetAllHandler);
group.MapPost("/", CreateHandler);
}
}
Step 3: Use Helper Extensions
private static async Task<Ok<Item>> CreateHandler(
[FromBody] CreateItemRequest request,
ClaimsPrincipal user,
ItemDataProvider dataProvider
)
{
// Extract user info with clean helpers
var userId = user.GetUserId();
var tenantId = user.GetTenantId();
var username = user.GetUsername();
var isAdmin = user.IsAdmin();
// Or get everything at once
var audit = user.GetAuditInfo();
var item = new Item
{
Name = request.Name,
CreatedBy = audit.UserId,
CreatedAt = audit.Timestamp,
TenantId = audit.TenantId
};
await dataProvider.CreateItem(item);
return TypedResults.Ok(item);
}
Complete Example: Catalog Component
Before (20+ lines of boilerplate):
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddAcsisDbContext<CatalogDb>("catalog");
// Manual JWT configuration
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var publicKey = builder.Configuration["JWT:PublicKey"];
using var rsa = RSA.Create();
rsa.ImportFromPem(publicKey);
var securityKey = new RsaSecurityKey(rsa.ExportParameters(false));
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "acsis-identity",
ValidateAudience = true,
ValidAudience = "acsis-api",
ValidateIssuerSigningKey = true,
IssuerSigningKey = securityKey,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapAcsisEndpoints(ItemApi.MapItemEndpoints);
app.Run();
After (1 line!):
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddAcsisDbContext<CatalogDb>("catalog");
builder.AddAcsisAuthentication(); // 🎉 That's it!
var app = builder.Build();
app.MapAcsisEndpoints(ItemApi.MapItemEndpoints);
app.Run();
Available Helper Extensions
ClaimsPrincipal Extensions
// Get user ID
Guid? userId = user.GetUserId();
// Get tenant ID (multi-tenancy)
Guid? tenantId = user.GetTenantId();
// Get username
string? username = user.GetUsername();
// Check if admin
bool isAdmin = user.IsAdmin();
// Get complete audit info
AuditInfo audit = user.GetAuditInfo();
// Properties: UserId, Username, TenantId, Timestamp
Query Extensions (Multi-Tenancy)
// Automatic tenant filtering
public async Task<List<Item>> GetAllItems(ClaimsPrincipal user)
{
return await _db.Items
.FilterByTenant(user) // ✨ Magic!
.ToListAsync();
}
// Your entity must implement ITenantScoped:
public class Item : ITenantScoped
{
public long Id { get; set; }
public string Name { get; set; }
public Guid? TenantId { get; set; } // Required by ITenantScoped
}
Pre-Configured Authorization Policies
// Available policies (no configuration needed!):
endpoints.MapPost("/items", CreateHandler)
.RequireAuthorization("CanManageItems");
endpoints.MapDelete("/items/{id}", DeleteHandler)
.RequireAuthorization("CanDeleteItems");
endpoints.MapPost("/users", CreateUserHandler)
.RequireAuthorization("CanAdministerUsers");
endpoints.MapGet("/admin/settings", GetSettingsHandler)
.RequireAuthorization("SystemAdministration");
endpoints.MapGet("/tenant-data", GetTenantDataHandler)
.RequireAuthorization("RequireTenant");
Policy Details:
| Policy | Required Roles |
|---|---|
CanManageItems |
AdvancedUser, Supervisor, SystemAdmin, SuperUser |
CanDeleteItems |
SystemAdmin, SuperUser |
CanAdministerUsers |
UserAdministrator, SystemAdmin, SuperUser |
SystemAdministration |
SystemAdmin, SuperUser |
RequireTenant |
Any authenticated user with a tenant_id claim |
Advanced Configuration
Custom Configuration
builder.AddAcsisAuthentication(options =>
{
options.ValidIssuer = "custom-issuer";
options.ValidAudience = "custom-audience";
options.ClockSkew = TimeSpan.FromMinutes(10);
options.RsaPublicKey = "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----";
});
Configuration from appsettings.json
{
"Authentication": {
"Acsis": {
"ValidIssuer": "acsis-identity",
"ValidAudience": "acsis-api",
"RsaPublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
"ClockSkew": "00:05:00"
}
}
}
Then just call:
builder.AddAcsisAuthentication(); // Reads from config automatically!
Service Discovery (Default)
If no configuration is provided, AddAcsisAuthentication() automatically uses service discovery to find the identity service:
// In development:
// - Authority: http://identity (via Aspire service discovery)
// - HTTPS not required
// In production:
// - Authority: https://identity
// - HTTPS required
Common Patterns
Pattern 1: Audit Trail
private static async Task<Created<Item>> CreateItemHandler(
[FromBody] CreateItemRequest request,
ClaimsPrincipal user,
ItemDataProvider dataProvider
)
{
var audit = user.GetAuditInfo();
var item = new Item
{
Name = request.Name,
Description = request.Description,
CreatedBy = audit.UserId,
CreatedAt = audit.Timestamp,
TenantId = audit.TenantId
};
await dataProvider.CreateItem(item);
return TypedResults.Created($"/items/{item.Id}", item);
}
Pattern 2: Multi-Tenant Data Access
public class ItemDataProvider(CatalogDb db)
{
public async Task<List<Item>> GetAllItems(ClaimsPrincipal user)
{
// Automatically filters by user's tenant
return await db.Items
.FilterByTenant(user)
.OrderBy(i => i.Name)
.ToListAsync();
}
public async Task<Item?> GetItemById(long id, ClaimsPrincipal user)
{
// Ensures user can only access items in their tenant
return await db.Items
.FilterByTenant(user)
.FirstOrDefaultAsync(i => i.Id == id);
}
}
Pattern 3: Role-Based Logic
private static async Task<Results<Ok<ItemList>, Forbidden>> GetAllItemsHandler(
ClaimsPrincipal user,
ItemDataProvider dataProvider
)
{
// Regular users see only active items
// Admins see everything
var items = user.IsAdmin()
? await dataProvider.GetAllItemsIncludingInactive(user)
: await dataProvider.GetActiveItems(user);
return TypedResults.Ok(items);
}
Pattern 4: Conditional Authorization
public static void MapItemEndpoints(this IEndpointRouteBuilder endpoints)
{
var items = endpoints.MapGroup("/items").WithTags("Items");
// Public endpoint
items.MapGet("/public", GetPublicItemsHandler)
.AllowAnonymous();
// Authenticated users
items.MapGet("/", GetAllItemsHandler)
.RequireAuthorization();
// Role-based
items.MapDelete("/{id}", DeleteItemHandler)
.RequireAuthorization("CanDeleteItems");
// Custom policy
items.MapPost("/admin/bulk-import", BulkImportHandler)
.RequireAuthorization("SystemAdministration");
}
Testing with Scalar UI
1. Start Your Service
dotnet run --project YourComponent/YourComponent.csproj
2. Get Access Token from Identity Service
Navigate to: https://localhost:40443/scalar
POST /auth/login:
{
"username": "admin",
"password": "password",
"profile_name": "Default"
}
Response:
{
"access_token": "eyJhbGci...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_at": "2025-01-07T12:00:00Z",
"user_id": 1,
"username": "admin"
}
3. Use Token in Your Service
In Scalar UI for your service (https://localhost:43443/scalar):
- Click "Authorize"
- Select "Bearer"
- Paste the
access_token - Click "Authorize"
All subsequent requests will include the token automatically!
Troubleshooting
Issue: 401 Unauthorized
Check:
- Is authentication configured? (
builder.AddAcsisAuthentication()) - Is token expired? (check
expires_atin login response) - Is token included in request? (check Authorization header)
Debug:
builder.AddAcsisAuthentication(); // Logs auth failures in development
Issue: 403 Forbidden
Check:
- Does user have required role?
- Is authorization policy configured correctly?
Solution:
// Check user's role in database
var adminRole = user.FindFirst("admin_role")?.Value;
Console.WriteLine($"User admin role: {adminRole}");
Issue: Tenant filtering not working
Check:
- Does entity implement
ITenantScoped? - Does user have a tenant_id claim?
Solution:
var tenantId = user.GetTenantId();
if (!tenantId.HasValue)
{
Console.WriteLine("Warning: User has no tenant ID");
}
What's Included
✅ One-line setup - builder.AddAcsisAuthentication()
✅ Auto middleware - UseAuthentication() and UseAuthorization() called automatically
✅ Helper extensions - GetUserId(), GetTenantId(), GetUsername(), IsAdmin(), GetAuditInfo()
✅ Query extensions - FilterByTenant() for automatic multi-tenant filtering
✅ Pre-configured policies - Common authorization policies ready to use
✅ Service discovery - Automatic identity service discovery via Aspire
✅ Debug logging - Token validation logging in development
Summary
Before ServiceDefaults Extensions
- 20+ lines of boilerplate per component
- Manual middleware registration
- Manual claim extraction (ugly!)
- No standardization across components
After ServiceDefaults Extensions
- 1 line to add authentication
- 0 lines for middleware (automatic)
- Clean helpers for all common operations
- Consistent across all Acsis components
Total effort per component: Add 1 line, protect endpoints, done! 🎉